1"use client";
2
3
17import { useState } from "react";
18import { useParams } from "next/navigation";
19import { useQueryClient } from "@tanstack/react-query";
20import { ArrowRightLeft, Loader2, Trash2 } from "lucide-react";
21import { toast } from "@/stores/ui-store";
22import { apiFetch } from "@/lib/api";
23import { useWorkspace, useWorkspaceMembers, useUpdateWorkspace } from "@/hooks/use-workspace";
24import { WorkspaceMember } from "@/lib/types";
25import { SettingsShell, Section, Field, inputCls } from "@/components/settings/settings-shell";
26
27export default function SettingsWorkspacePage() {
28 const params = useParams<{ workspace: string }>();
29 const slugOrId = params?.workspace ?? "";
30 const { data: ws, isLoading } = useWorkspace(slugOrId);
31 const qc = useQueryClient();
32
33 const [name, setName] = useState<string | null>(null);
34 const [emoji, setEmoji] = useState<string | null>(null);
35 const update = useUpdateWorkspace(ws?.id ?? "");
36 const displayName = name ?? ws?.name ?? "";
37 const displayEmoji = emoji ?? ws?.emoji ?? "";
38
39
40
41
42 const role = (ws?.role ?? "").toLowerCase();
43 const isOwner = role === "owner";
44 const isAdminOrOwner = role === "owner" || role === "admin";
45
46 return (
47 <SettingsShell title="Workspace settings" description={ws ? `${ws.name} · ${ws.plan} plan` : "Configure this workspace."} active="">
48 {isLoading || !ws ? (
49 <p className="text-sm text-muted-foreground">Loading…</p>
50 ) : (
51 <div className="space-y-5">
52 {}
53 <Section title="General" description="Rename the workspace.">
54 <div className="flex items-end gap-3">
55 <div>
56 <Field label="Emoji">
57 <input className={`${inputCls} max-w-16 text-center text-2xl`} value={displayEmoji} maxLength={4} onChange={(e) => setEmoji(e.target.value)} />
58 </Field>
59 </div>
60 <div className="flex-1">
61 <Field label="Name">
62 <input className={inputCls} value={displayName} onChange={(e) => setName(e.target.value)} />
63 </Field>
64 </div>
65 <button
66 type="button"
67 className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
68 disabled={update.isPending || (displayName === ws.name && displayEmoji === (ws.emoji ?? ""))}
69 onClick={() =>
70 update.mutate(
71 { name: displayName.trim(), emoji: displayEmoji || null },
72 {
73 onSuccess: () => toast("success", "Workspace updated"),
74 onError: (e) => toast("error", "Update failed", (e as Error).message)
75 }
76 )
77 }
78 >
79 {update.isPending ? <Loader2 className="size-3.5 animate-spin" /> : "Save"}
80 </button>
81 </div>
82 </Section>
83
84 {}
85 <Section title="Members">
86 <p className="text-sm text-muted-foreground">
87 {ws.memberCount} member{ws.memberCount === 1 ? "" : "s"} ·{" "}
88 <a className="text-primary hover:underline" href={`/w/${slugOrId}/settings/members`}>
89 Manage members & invitations
90 </a>
91 </p>
92 </Section>
93
94 {}
95 {isAdminOrOwner && (
96 <Section
97 danger
98 title="Danger zone"
99 description={isOwner ? "You can transfer ownership or delete this workspace." : "Only the owner can transfer ownership or delete this workspace."}
100 >
101 <div className="space-y-4">
102 <TransferOwnership disabled={!isOwner} workspaceId={ws.id} />
103 <DeleteWorkspace disabled={!isOwner} workspaceId={ws.id} workspaceName={ws.name} />
104 </div>
105 </Section>
106 )}
107 </div>
108 )}
109 </SettingsShell>
110 );
111}
112
113function TransferOwnership({ disabled, workspaceId }: { disabled: boolean; workspaceId: string }) {
114 const members = useWorkspaceMembers(workspaceId);
115 const qc = useQueryClient();
116 const [target, setTarget] = useState("");
117 const [busy, setBusy] = useState(false);
118
119 const options = (members.data ?? []).filter((m: WorkspaceMember) => m.userId !== target);
120
121 const transfer = async () => {
122 if (!target) return;
123 setBusy(true);
124 try {
125 await apiFetch(`/workspaces/${workspaceId}/transfer`, { method: "POST", body: { newOwnerId: target } });
126 toast("success", "Ownership transferred");
127 qc.invalidateQueries({ queryKey: ["workspaces", workspaceId] });
128 } catch (e) {
129 toast("error", "Transfer failed", (e as Error).message);
130 } finally {
131 setBusy(false);
132 }
133 };
134
135 return (
136 <div className="flex items-end gap-2">
137 <div className="flex-1">
138 <Field label="New owner" hint={disabled ? "Owner only." : "The new owner must already be a member."}>
139 <select className={inputCls} value={target} onChange={(e) => setTarget(e.target.value)} disabled={disabled}>
140 <option value="">Select a member…</option>
141 {options.map((m) => (
142 <option key={m.userId} value={m.userId}>
143 {m.user.name} ({m.role})
144 </option>
145 ))}
146 </select>
147 </Field>
148 </div>
149 <button
150 type="button"
151 className="inline-flex items-center gap-1.5 rounded-md border border-destructive/40 px-3 py-1.5 text-sm text-destructive hover:bg-destructive/10 disabled:opacity-50"
152 disabled={disabled || !target || busy}
153 onClick={transfer}
154 >
155 <ArrowRightLeft className="size-3.5" /> {busy ? "Transferring…" : "Transfer"}
156 </button>
157 </div>
158 );
159}
160
161function DeleteWorkspace({ disabled, workspaceId, workspaceName }: { disabled: boolean; workspaceId: string; workspaceName: string }) {
162 const qc = useQueryClient();
163 const [confirmText, setConfirmText] = useState("");
164 const [busy, setBusy] = useState(false);
165
166 const del = async () => {
167 setBusy(true);
168 try {
169 await apiFetch(`/workspaces/${workspaceId}`, { method: "DELETE" });
170 toast("success", "Workspace deleted");
171 qc.removeQueries({ queryKey: ["workspaces", workspaceId] });
172 if (typeof window !== "undefined") window.location.href = "/";
173 } catch (e) {
174 toast("error", "Delete failed", (e as Error).message);
175 setBusy(false);
176 }
177 };
178
179 return (
180 <div>
181 <Field label="Delete workspace" hint="Type the workspace name to confirm. This permanently removes all pages, databases, files, and members.">
182 <input
183 className={inputCls}
184 value={confirmText}
185 onChange={(e) => setConfirmText(e.target.value)}
186 placeholder={workspaceName}
187 disabled={disabled}
188 />
189 </Field>
190 <button
191 type="button"
192 className="mt-2 inline-flex items-center gap-1.5 rounded-md bg-destructive px-3 py-1.5 text-sm text-white hover:bg-destructive/90 disabled:opacity-50"
193 disabled={disabled || confirmText.trim() !== workspaceName || busy}
194 onClick={del}
195 >
196 <Trash2 className="size-3.5" /> {busy ? "Deleting…" : "Delete workspace"}
197 </button>
198 </div>
199 );
200}
201